#include "include/dev.h"

#include <stdio.h>


#ifdef UNIX
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <sys/ioctl.h>
    #include <sys/termios.h>
    #include <sys/mman.h>
#endif

#ifdef WINDOWS
    #include <conio.h>
    #include <windows.h>
    #define ENABLE_VIRTUAL_TERMINAL_PROCESSING  0x0004
#endif

/**
 * 定义变量保存物理机终端属性
 */ 
#ifdef UNIX
    struct termios original_tio;
#endif

#ifdef WINDOWS
    DWORD original_out_dwMode;
    DWORD original_in_dwMode;
#endif

MDev::MDev()
{
    outstream = (void *)stdout;
    instream  = (void *)stdin;
}

MDev::~MDev()
{
    if(outstream && (FILE *)outstream != stdout) {
        fclose( (FILE *)outstream );
        outstream = nullptr;
    }
    if(instream && (FILE *)instream != stdin) {
        fclose( (FILE *)instream);
        instream = nullptr;
    }
}

/**
 * @brief : 
 *      按键检测
 * @return : 
 *      int : 若按键按下，返回非零，否者返回零
 */      
int MDev::check_key()
{
    int ret = 0;
    #ifdef UNIX
        fd_set readfds;
        FD_ZERO(&readfds);               // 将一个文件描述符集变量的所有位均设为0
        FD_SET(STDIN_FILENO, &readfds);  // 根据标准输入设置文件描述符集

        struct timeval timeout;
        timeout.tv_sec = 0;              // 秒
        timeout.tv_usec = 0;             // 微秒

        /**
         * selectc解析
         * 第一个参数：文件描述符集中文件最大的文件描述符编号
         * 第二个参数：指向可读文件描述符集的指针
         * 第三个参数：指向可写文件描述符集的指针
         * 第四个参数：指向异常文件描述符集的指针
         * 第五个参数：指向等待时间timeval的指针，单位为为秒和微秒，NULL表示永远等待
         * 返回值：准备就绪的文件描述符数目，若超时返回0，若出错返回-1
         */ 
        ret = ( select(1, &readfds, NULL, NULL, &timeout) != 0 ) ? 1 : 0;
    #endif
    #ifdef WINDOWS
        ret = _kbhit();
    #endif
    return ret;
}


/**
 * @brief : 
 *      备份物理机终端属性，设置虚拟机终端属性
 */  
void MDev::set_terminal()
{
    #ifdef UNIX
        tcgetattr(STDIN_FILENO, &original_tio);
        struct termios new_tio = original_tio;

        /**
         * c_lflag 表示本地终端标志（local flag）
         * ICANON  表示规范输入，即以行为单位输入
         * ECHO    表示启用回显
         */ 
        new_tio.c_lflag &= ~ICANON & ~ECHO;     // 关闭规范输入和回显

        /**
         * 设置新的终端属性
         * TCSANOW 表示更改立即生效
         */ 
        tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
    #endif
    #ifdef WINDOWS
        /* 备份物理机控制台输入输出模式 */
        HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
        HANDLE hIn  = GetStdHandle(STD_INPUT_HANDLE);
        GetConsoleMode(hOut, &original_out_dwMode);
        GetConsoleMode(hIn,  &original_in_dwMode);

        /* 设置LC3虚拟机终端输入输出模式 */
        DWORD  new_out_dwMode = original_out_dwMode;
        DWORD  new_in_dwMode = original_in_dwMode;
        new_out_dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;      // 启用ANSI输出
        new_in_dwMode &= ~ENABLE_ECHO_INPUT & ~ENABLE_LINE_INPUT;  // 关闭规范输入和回显
        SetConsoleMode(hOut, new_out_dwMode);
        SetConsoleMode(hIn, new_in_dwMode);
    #endif
}

/**
 * @brief: 
 *      恢复物理机终端属性
 */ 
void MDev::restore_terminal()
{
    /* 刷新流 */
    flush();

    #ifdef UNIX
        tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
    #endif
    #ifdef WINDOWS
        /* 恢复物理机控制台输入输出模式 */
        HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
        HANDLE hIn  = GetStdHandle(STD_INPUT_HANDLE);
        SetConsoleMode(hOut, original_out_dwMode);
        SetConsoleMode(hIn, original_in_dwMode);
    #endif
}

/**
 * @brief : 
 *      从标准设备输入一个字符
 * @return :  
 *      char : 返回输入的字符
 */
char MDev::inchar()
{
    return getchar();
}

/**
 * @brief 
 *      向标准设备输出一个字符
 * @param c 
 *      char c : 待输出的字符
 */     
void MDev::outc(char c)
{
    putc(c, (FILE *)outstream);
}

/**
 * @brief 
 *      向标准设备输出一个字符串
 * @param str 
 *      const char* str : 带输出的字串
 */
void MDev::outs(const char* str)
{
    puts(str);
}

/**
 * @brief 
 *      刷新输入输出流
 */
void MDev::flush()
{
    fflush( (FILE*)outstream );
    fflush( (FILE*)instream );
}

///////////////////////////////////////////////////////////////////////////
////////////////////////// 虚拟机终端 //////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

Terminal::Terminal()
{
    #ifdef UNIX
        struct winsize size;
        if( ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)size) ) {
            fprintf(stderr, "TIOCGWINSZ error");
            flag = 1;
        }
        tws_row = size.ws_row;
        tws_col = size.ws_col;
    #endif

    #ifdef WINDOWS
        HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_SCREEN_BUFFER_INFO binfo;
        GetConsoleScreenBufferInfo(hout, &binfo);
        tws_row = binfo.dwSize.Y;
        tws_col = binfo.dwSize.X;
    #endif

    /* 创建终端缓冲区 */
    createBuffer();

    /* 初始化终端缓冲区光标位置 */
    cursor_x = 0;
    cursor_y = 0;

    /* 初始化输入输出流 */
    outstream = (void *)stdout;
    instream  = (void *)stdin;
}

Terminal::Terminal(int width, int height)
{
    tws_row = width;
    tws_col = height;
    createBuffer();

    cursor_x = 0;
    cursor_y = 0;

    outstream = (void *)stdout;
    instream  = (void *)stdin;
}

Terminal::~Terminal()
{
    freeBuffer();

    if(outstream && (FILE *)outstream != stdout) {
        fclose( (FILE *)outstream );
        outstream = nullptr;
    }
    if(instream && (FILE *)instream != stdin) {
        fclose( (FILE *)instream);
        instream = nullptr;
    }
}

void Terminal::createBuffer()
{
    int flag = 0;
    if(tws_row > 0 && tws_col > 0) {
        buffer = (char **)calloc(tws_row, sizeof(char *));
        if(buffer) {
            for(int i = 0; i < tws_row; i++) {
                buffer[i] = (char *)calloc(tws_col, sizeof(char));
                flag = ( buffer[i] ? 0 : 1 );
            }
        }else {
            /* 缓冲区空间申请失败 */
            flag = 1;
        }
    }else {
        /* 缓冲区尺寸有误 */
        flag = 1;
    }

    if(flag) {
        fprintf(stderr, "create terminal<%d, %d> fail.", tws_row, tws_col);
        exit(-1);
    }
}

void Terminal::freeBuffer()
{
    if(buffer) {
        for(int i = 0; i < tws_row; i++) {
            if(buffer[i]) {
                free(buffer[i]);
                buffer[i] = nullptr;
            }
        }
        buffer = nullptr;
    }
}

char Terminal::inchar()
{
    return getchar();
}


void Terminal::outc(char c)
{
    if( cursor_x == tws_col && cursor_y == (tws_row - 1) ) {
        flush();
    }

    cursor_y = (cursor_x == tws_col ? cursor_y + 1 : cursor_y);
    cursor_x = (cursor_x == tws_col ? 0 : cursor_x);

    buffer[cursor_y][cursor_x] = c; 
    cursor_x += 1;
}

void Terminal::outs(const char *str)
{
    for(int i = 0; str[i]; i++) {
        outc(str[i]);
    }
}

void Terminal::flush()
{
    for(int i = 0; i < tws_row; i++) {
        for(int j = 0; j < tws_col; j++) {
            putc(buffer[i][j], (FILE *)outstream);
            buffer[i][j] = 0;
        }
    }
    cursor_x = 0;
    cursor_y = 0;
    fflush((FILE *)outstream);
    fflush((FILE *)instream);
}